ci(release): authenticate prepared artifacts - #24
Conversation
Bind crate and GitHub publication to one verified prepare run, align release-time manifest and lockfile stamping transactionally, and pin the transitive release action surface. Add executable release contracts plus the maintainer runbook so provenance, artifact completeness, retry behavior, and dependency boundaries remain enforced.
|
Reviewed this rebased onto current The provenance document does not add what the description claims. The genuinely useful part is the exact-five-archive filename assertion in Two changes are regressions:
The
Action SHA pinning is a legitimate practice and the pins are correctly annotated with their tags. It is not urgent here, it is a standing maintenance commitment that wants Dependabot or Renovate alongside it, and it does not belong bundled with 900 lines of unrelated machinery. Happy to take it as a standalone change. If this is worth revisiting, the shape is a small commit carrying the archive-name assertion and the tag-commit check, and a separate one for pinning. |
|
Reworked into the two requested independent lanes:
The closed branch remains untouched as review history; neither replacement depends on it. |
Summary
This PR makes PageDB's two-stage release process fail closed around the exact
source and binary artifacts that were prepared for a tag.
The current workflow already separates an expensive tag-triggered preparation
run from a manually dispatched distribution run. That is the right overall
shape, but the preparation run ID is currently treated as sufficient evidence
that the downloaded files belong to the requested release. Crate publication
also validates the tag without depending on the preparation run at all. A
maintainer can therefore select a successful but unrelated run, a partial
artifact set can reach distribution, and a prerelease version stamp can leave
Cargo.lockinconsistent withCargo.toml.The new contract is:
v*tag is validated against the package base version and resolvedto its peeled commit.
Release Prepareruns the complete reusable CI workflow, builds the fivesupported
pagedb-fsckarchives once, and records a small immutableprovenance document.
run's provenance and archives, and requires exact agreement on repository,
tag, version, commit, and run ID.
missing, extra, renamed, or non-file entries.
shared acceptance gate, while remaining independently retryable.
The change is intentionally confined to release automation, executable release
contracts, the tracked lockfile metadata needed by locked packaging, and
maintainer documentation. It does not modify PageDB runtime Rust code, storage
algorithms, database behavior, file formats, browser code, or benchmark
implementations.
Why this is needed
A run ID is a locator, not provenance
actions/download-artifactcan select another workflow run by numeric ID, butthe successful download only proves that the run exists and contains artifacts
matching the requested pattern. It does not prove that the run was created from
this repository, for this tag, from this commit, with this package version.
Release Preparenow uploadsrelease-provenance.jsonwith:The distribution workflow checks every field against values independently
derived from the requested release. The run ID still locates the artifacts, but
the content establishes whether those artifacts are acceptable.
Publication must not bypass preparation
Previously, the GitHub Release stage consumed prepared artifacts while the
crates.io stage depended only on tag validation. That allowed the two outputs of
one release to cross different trust boundaries.
This PR introduces a shared
validate-preparejob. Both publication jobsdeclare:
The crate and binary stages remain independent after that point. A crates.io
outage does not require rebuilding binaries, and a GitHub Release-body retry
does not require republishing the crate. A retry reuses the same accepted tag
and preparation run.
Locked prerelease stamping must update both files
The current version stamper edits the first
version = "..."line inCargo.toml. There are two problems with that approach:[package]field;
entry in
Cargo.lockstill carries the base version.That second case is observable with
cargo package --locked: Cargo requests alockfile update and the release fails after the tag has already been prepared.
The replacement stamper treats
Cargo.tomlandCargo.lockas onetransactional release unit. It:
X.Y.ZorX.Y.Z-label.N;[package]version;--allow-dirtyremains necessary for the deliberate release-time manifest andlock edits. It does not disable Cargo's package verification. Build, metadata,
package, and publish operations remain locked.
Artifact acceptance contract
The provenance verifier expects exactly these versioned files:
pagedb-fsck-${VERSION}-linux-x64.tar.gz;pagedb-fsck-${VERSION}-linux-arm64.tar.gz;pagedb-fsck-${VERSION}-macos-arm64.tar.gz;pagedb-fsck-${VERSION}-macos-x64.tar.gz;pagedb-fsck-${VERSION}-windows-x64.zip.The directory must contain five entries, each entry must be a regular file, and
each expected name must exist. This rejects both an incomplete matrix and an
over-broad artifact glob that introduces unexpected release attachments.
The macOS x64 archive continues to cross-compile on the current Apple Silicon
macos-latestrunner. No dependency on the retired Intel runner isreintroduced.
Workflow and supply-chain hardening
All third-party actions touched by the release path and its transitively invoked
test workflow are pinned to immutable commits. Each pin was checked against the
corresponding official ref immediately before preparing this PR:
actions/checkoutv7:3d3c42e5aac5ba805825da76410c181273ba90b1;actions/upload-artifactv7:043fb46d1a93c77aae656e7c1c64a875d1fc6a0a;actions/download-artifactv8:3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c;softprops/action-gh-releasev3 peeled commit:3d0d9888cb7fd7b750713d6e236d1fcb99157228;dtolnay/rust-toolchainstable:4cda84d5c5c54efe2404f9d843567869ab1699d4;Swatinem/rust-cachev2 peeled commit:e18b497796c12c097a38f9edb9d0641fb99eee32;taiki-e/install-actionv2.85.2:41049aa56687c35e0afa74eed4f09cec4f9afabf.Every checkout disables persisted credentials. The release artifact build does
not restore the Rust build cache, avoiding cache state in the bytes promoted to
a release.
Matrix values are passed through environment variables and a fixed
configuration
caserather than interpolated directly into shell source. Thisremoves template-injection findings without changing the target or feature
coverage.
Permissions are job-scoped and documented inline. The preparation workflows are
read-only. Artifact validation receives
actions: read; GitHub Releasecreation receives
contents: write; crates.io publication continues to use theprotected
crates.ioenvironment and its existing registry token.The release workflows do not install or depend on RocksDB, SQLite, redb,
Clang, libclang, or any external comparison engine. The benchmark job still
installs its own compilation prerequisite inside the reusable test workflow,
where that dependency belongs; it is not a prerequisite of release packaging
or publication.
Executable regression coverage
Three shell contract suites are added to the reusable CI workflow.
scripts/test-stamp-version.shcovers:[package]selection without modifying metadata versions;scripts/test-release-provenance.shcreates an independent fixture and covers:scripts/test-release-workflow-contract.shcovers the workflow wiring itself:These are behavior and boundary assertions rather than snapshots of complete
YAML files. They are narrow enough to explain a failure and broad enough to
prevent the release trust boundary from being removed by a later workflow
refactor.
Documentation
RELEASING.mdis the operator runbook for the two-stage process. It documents:mechanism.
The README links to the runbook and states the prepare-then-distribute model.
Verification
The candidate is based directly on
NodeDB-Lab/pagedb@f9e6cfaee12a0ebb2436f5cdf81f88273ad582d2.That hash was rechecked against the upstream
mainref immediately before thefinal review.
Release contracts and workflow analysis
bash scripts/test-stamp-version.sh: passed.bash scripts/test-release-provenance.sh: passed.bash scripts/test-release-workflow-contract.sh: passed.actionlintYAML/expression/workflow validation: passed on all four changedworkflows.
shellcheckon all five changed shell scripts: passed.shellcheckon the inline release validation and distribution shell blocks:passed.
git diff --check: passed.SHORTCUT:markers are present.Native Rust regression and quality gates
cargo nextest run -p pagedb --all-features --no-fail-fast:589 passed, 5 skipped, 0 failed.
PAGEDB_INVARIANT_CHECKS=1:589 passed, 5 skipped, 0 failed.
cargo fmt --all --check: passed.cargo clippy -p pagedb --all-targets --all-features -- -D warnings:passed.
cargo doc -p pagedb --no-deps --all-features: passed.cargo test -p pagedb --doc --all-features: passed.cargo deny check: advisories, bans, licenses, and sources passed. Itsexisting unmatched-license and duplicate-version notices remain warnings.
cargo package -p pagedb --locked --allow-dirty: passed and verified thepackaged crate; 214 files, approximately 1.8 MiB unpacked and 398.3 KiB
compressed.
The first local Clippy attempt exposed a machine-level Homebrew-versus-rustup
compiler cache mismatch. No source was changed to hide it. Re-running with the
rustup toolchain directory first in
PATHrebuilt the affected metadata andpassed. This is recorded here so the successful result is not mistaken for an
unexplained retry.
Feature and target matrix
All four feature configurations passed for library, binaries, tests, and
benches:
compression;Cross-target library checks passed for:
x86_64-pc-windows-gnu;aarch64-apple-darwin;x86_64-apple-darwin;aarch64-apple-ios;aarch64-linux-android;armv7-linux-androideabi;wasm32-unknown-unknownwithopfs;wasm32-wasip1.The two WebAssembly targets emit existing unused/dead-code warnings for
native-only members, but both checks exit successfully. This PR does not change
those Rust modules.
Dependency isolation passed for both default and all-feature PageDB graphs:
neither RocksDB nor rusqlite appears in the PageDB package's dependency tree.
Benchmark availability and performance disposition
All benchmark executables still build:
segment;btree;comparison, including its RocksDB and SQLitecomparison dependencies.
This PR has no production Rust or benchmark diff, so it introduces no runtime
performance path to compare against upstream. Re-running wall-clock benchmarks
would compare the same PageDB Rust sources with local system noise and would not
measure the release workflow changes. The compile-only benchmark gate is the
relevant regression proof for this lane.
Scope boundaries
This PR does not:
packages to PageDB;
public APIs;
The work was prepared in an isolated checkout. The original dirty PageDB
checkout and its uncommitted work were not modified.
Suggested review order
scripts/ci/verify_release_provenance.shand its fixture test for theacceptance boundary.
.github/workflows/release.ymlfor the shared gate and independentlyretryable publication stages.
.github/workflows/release-prepare.ymland.github/workflows/release-validate.ymlfor the values authenticated by thatgate.
scripts/ci/stamp_version.shand its transactional regression test.scripts/test-release-workflow-contract.shand the release-contract entry in.github/workflows/test.yml.RELEASING.md, README, and the two-lineCargo.lockregistry metadata fixrequired by locked package verification.